import { compare } from 'bcrypt';
import { sign } from 'jsonwebtoken';
import { Service, Container } from 'typedi';
import { SECRET_KEY, DOUBLE_TOKEN, ACCESS_TOKEN_EXPIRATION, REFRESH_TOKEN_EXPIRATION } from '@config';
import { LoginUserDto } from '@/dtos';
import { DataStoredInToken, TokenData } from '@/interfaces';
import { PrismaClient } from '@/generated/prisma';
import Redis from 'ioredis';
import { LoginUserInfoFormatted, normalizeUserData } from '@utils/formatter';
import { InvalidCredentialsException } from '@/exceptions';
import { Request } from 'express';

const hoursToSeconds = (hours: string): number => parseInt(hours) * 60 * 60;

const createToken = async (user: LoginUserInfoFormatted, redis: Redis, refresh: boolean = false): Promise<TokenData> => {
  const { departmentId, departmentName, email, name, username, id, roles } = user;
  const dataStoredInToken: DataStoredInToken = { departmentId, email, name, username, id, roles, departmentName };
  const accessTokenExpiration: number = hoursToSeconds(ACCESS_TOKEN_EXPIRATION || '0');
  const refreshTokenExpiration: number = hoursToSeconds(REFRESH_TOKEN_EXPIRATION || '0');
  let accessToken: string, refreshToken: string;
  accessToken = sign({ ...dataStoredInToken, type: 'accessToken' }, SECRET_KEY, { expiresIn: accessTokenExpiration });
  await redis.set(`${username}:token`, accessToken, 'EX', accessTokenExpiration);
  if (!DOUBLE_TOKEN) return { accessTokenExpiration, accessToken };
  let _refreshTokenExpiration: number = refreshTokenExpiration;
  if (refresh) _refreshTokenExpiration = await redis.ttl(`${username}:refreshToken`);
  refreshToken = sign(
    {
      ...dataStoredInToken,
      type: 'refreshToken',
    },
    SECRET_KEY,
    { expiresIn: _refreshTokenExpiration },
  );
  await redis.set(`${username}:refreshToken`, refreshToken, 'EX', _refreshTokenExpiration);
  return { accessTokenExpiration, accessToken, refreshTokenExpiration: _refreshTokenExpiration, refreshToken };
};

@Service()
export class AuthService {
  public token_redis: Redis = Container.get('REDIS_TOKEN');
  public online_redis: Redis = Container.get('REDIS_ONLINE');
  public prisma: PrismaClient = Container.get('PRISMA_CLIENT');

  public async login(req: Request, userData: LoginUserDto) {
    const findUser = await this.prisma.user.findFirst({
      where: { OR: [{ email: userData.email }, { username: userData.username }] },
      include: {
        department: { select: { id: true, name: true, parent: { select: { id: true, name: true } } } },
        roles: { include: { role: { select: { code: true, name: true } } } },
      },
    });

    if (!findUser) throw new InvalidCredentialsException();

    const isPasswordMatching: boolean = await compare(userData.password, findUser.password);
    if (!isPasswordMatching) {
      await this.prisma.user.update({
        where: { id: findUser.id },
        data: { passwordErrorCount: ++findUser.passwordErrorCount, passwordErrorTime: new Date() },
      });
      throw new InvalidCredentialsException();
    }
    const lastLoginIp = req.ip || req.socket.remoteAddress || req.connection.remoteAddress || '无法获取IP';
    await this.prisma.user.update({
      where: { id: findUser.id },
      data: { passwordErrorCount: 0, passwordErrorTime: null, lastLoginIp: lastLoginIp, lastLoginTime: new Date() },
    });

    const normalized = normalizeUserData(findUser);

    const { accessToken, refreshToken } = await createToken(normalized, this.token_redis);
    if (!DOUBLE_TOKEN) return { findUser, accessToken, isDoubleToken: Boolean(DOUBLE_TOKEN) };
    return { normalized, accessToken, refreshToken, isDoubleToken: Boolean(DOUBLE_TOKEN) };
  }

  public async logout(userData: LoginUserInfoFormatted) {
    await this.token_redis.del(`${userData.username}:token`);
    await this.token_redis.del(`${userData.username}:refreshToken`);
    await this.online_redis.del(`online:${userData.id}`);
  }

  public async refresh(userData: LoginUserInfoFormatted) {
    const { accessToken, refreshToken } = await createToken(userData, this.token_redis, true);
    if (!DOUBLE_TOKEN) return { accessToken, isDoubleToken: Boolean(DOUBLE_TOKEN) };
    return { accessToken, refreshToken, isDoubleToken: Boolean(DOUBLE_TOKEN) };
  }

  public async heartbeat(userData: LoginUserInfoFormatted) {
    await this.online_redis.set(`online:${userData.id}`, userData.username, 'EX', 300);
  }
}
